/*
 * 
 *  This file is part of Sourceprojects.org Lycia Copyright 2010 noctarius
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * 
 */
package org.sourceprojects.lycia.internal;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Implementation of the {@link Cache} interface with {@link SoftReference}d
 * values which can be removed by GarbageCollector if there is no memory left.
 * 
 * @author noctarius
 * @since 1.9.1
 */
public class SoftReferencedCache<T> implements Cache<T> {
	private final AtomicReference<ScheduledFuture<?>> scheduledFuture = new AtomicReference<ScheduledFuture<?>>();
	private final Map<String, SoftReference<CacheEntry<T>>> cache = new ConcurrentHashMap<String, SoftReference<CacheEntry<T>>>();
	private final ReentrantLock lock = new ReentrantLock();
	private final ReferenceQueue<CacheEntry<T>> queue = new ReferenceQueue<CacheEntry<T>>();
	private final DelayQueue<DelayedRemove<T>> removerQueue = new DelayQueue<DelayedRemove<T>>();
	private final AtomicBoolean initialized = new AtomicBoolean(false);
	private static final long NANO_ORIGIN = System.nanoTime();

	private final long expireTime;

	private boolean selfCreated = false;
	private ScheduledExecutorService executorService = null;

	/**
	 * Creates a basic softreferenced cache with basic expiration time of 10
	 * seconds
	 * 
	 * @param executorService
	 *            ScheduledExecutorService to use for remover task
	 */
	public SoftReferencedCache() {
		this(null, 10, TimeUnit.SECONDS);
	}

	/**
	 * Creates a basic softreferenced cache with basic expiration time of 10
	 * seconds
	 * 
	 * @param executorService
	 *            ScheduledExecutorService to use for remover task
	 */
	public SoftReferencedCache(final ScheduledExecutorService executorService) {
		this(executorService, 10, TimeUnit.SECONDS);
	}

	public SoftReferencedCache(final long expireTime, final TimeUnit unit) {
		this(null, expireTime, unit);
	}

	public SoftReferencedCache(final ScheduledExecutorService executorService,
			final long expireTime, final TimeUnit unit) {

		this.expireTime = unit.convert(expireTime, TimeUnit.MILLISECONDS);
		this.executorService = executorService;
	}

	@Override
	public T get(final String name) {
		removeQueuedSoftReferences();
		final SoftReference<CacheEntry<T>> reference = cache.get(name);
		return reference == null || reference.get() == null ? null : reference
				.get().getObject();
	}

	@Override
	@SuppressWarnings("unchecked")
	public <R extends T> R get(final String name, final Class<R> type) {
		removeQueuedSoftReferences();
		final SoftReference<CacheEntry<T>> reference = cache.get(name);
		return (R) (reference == null || reference.get() == null ? null
				: reference.get().getObject());
	}

	@Override
	public long getExpire() {
		return expireTime;
	}

	@Override
	public void put(final String name, final T obj) {
		initializeTimer();
		final CacheEntry<T> entry = new CacheEntry<T>(obj, expireTime,
				TimeUnit.MILLISECONDS);
		cache.put(name, new SoftReference<CacheEntry<T>>(entry, queue));
		removerQueue.add(new DelayedRemove<T>(name, entry));
	}

	@Override
	public void put(final String name, final T obj, final long expireTime) {
		initializeTimer();
		final CacheEntry<T> entry = new CacheEntry<T>(obj, expireTime,
				TimeUnit.MILLISECONDS);
		cache.put(name, new SoftReference<CacheEntry<T>>(entry, queue));
		removerQueue.add(new DelayedRemove<T>(name, entry));
	}

	final long now() {
		return System.nanoTime() - NANO_ORIGIN;
	}

	private void initializeTimer() {
		try {
			lock.lock();
			if (!initialized.get()) {
				if (executorService == null) {
					selfCreated = true;
					executorService = Executors.newScheduledThreadPool(5);
				}

				// Initialize RemoverTask
				scheduledFuture.set(executorService.scheduleAtFixedRate(
						new RemoverTask(removerQueue), 1, 1, TimeUnit.SECONDS));

				initialized.set(true);
			}

		} finally {
			lock.unlock();
		}
	}

	private synchronized void removeQueuedSoftReferences() {
		Reference<? extends CacheEntry<T>> reference = null;
		// Try to poll next candidate from queue
		while ((reference = queue.poll()) != null) {
			String keyToRemove = null;
			for (final String key : cache.keySet()) {
				final SoftReference<CacheEntry<T>> r = cache.get(key);

				if (r.equals(reference)) {
					// Found key, save to remove
					keyToRemove = key;
					break;
				}
			}

			// Remove if key was found
			if (keyToRemove != null) {
				cache.remove(keyToRemove);
			}
		}
	}

	private class RemoverTask implements Runnable {
		private final AtomicBoolean alreadyRunning = new AtomicBoolean(false);
		private final DelayQueue<DelayedRemove<T>> removeQueue;

		RemoverTask(final DelayQueue<DelayedRemove<T>> removeQueue) {
			this.removeQueue = removeQueue;
		}

		@Override
		public void run() {
			if (alreadyRunning.getAndSet(true)) {
				return;
			}

			DelayedRemove<T> removeEntry = null;
			while ((removeEntry = removeQueue.poll()) != null) {
				cache.remove(removeEntry.getName());
			}

			alreadyRunning.set(false);
		}
	}

	private class DelayedRemove<I> implements Delayed {
		private final CacheEntry<I> cacheEntry;
		private final String name;

		DelayedRemove(final String name, final CacheEntry<I> cacheEntry) {
			this.name = name;
			this.cacheEntry = cacheEntry;
		}

		@Override
		public long getDelay(final TimeUnit unit) {
			return unit.convert(cacheEntry.getExpirationTime() - now(),
					TimeUnit.NANOSECONDS);
		}

		@Override
		public int compareTo(final Delayed other) {
			if (other == this) {
				return 0;
			}

			final long d = (getDelay(TimeUnit.NANOSECONDS) - other
					.getDelay(TimeUnit.NANOSECONDS));

			return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
		}

		public String getName() {
			return name;
		}
	}

	private class CacheEntry<I> {
		private final I object;
		private long lastAccessTime;
		private final long expiration;

		private CacheEntry(final I object, final long expiration,
				final TimeUnit unit) {
			this.object = object;
			this.expiration = unit.convert(expiration, TimeUnit.NANOSECONDS);
		}

		I getObject() {
			lastAccessTime = System.nanoTime() - NANO_ORIGIN;
			return object;
		}

		long getExpirationTime() {
			return (lastAccessTime + expiration) - now();
		}
	}

	@Override
	public void shutdown() {
		cache.clear();

		if (selfCreated && executorService != null) {
			executorService.shutdownNow();
		} else {
			if (scheduledFuture.get() != null) {
				scheduledFuture.get().cancel(false);
			}
		}
	}

	@Override
	public void invalidate() {
		cache.clear();
	}

}
